/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */

/*
 * Jython.java
 * Copyright (C) 2007-2012 University of Waikato, Hamilton, New Zealand
 */

package weka.core.scripting;

import java.io.File;
import java.io.InputStream;
import java.io.Serializable;
import java.lang.reflect.Constructor;
import java.lang.reflect.Method;
import java.util.HashSet;

/**
 * A helper class for
 * <a href="http://www.jython.org/" target="_blank">Jython</a>.
 * 
 * @author fracpete (fracpete at waikato dot ac dot nz)
 * @version $Revision$
 */
public class Jython implements Serializable {

    /** for serialization */
    private static final long serialVersionUID = -6972298704460209252L;

    /** the classname of the Python interpreter */
    public final static String CLASS_PYTHONINERPRETER = "org.python.util.PythonInterpreter";

    /** the classname of the Python ObjectInputStream */
    public final static String CLASS_PYTHONOBJECTINPUTSTREAM = "org.python.util.PythonObjectInputStream";

    /** whether the Jython classes are in the Classpath */
    protected static boolean m_Present = false;
    static {
        try {
            Class.forName(CLASS_PYTHONINERPRETER);
            m_Present = true;
        } catch (Exception e) {
            m_Present = false;
        }
    }

    /** the interpreter */
    protected Object m_Interpreter;

    /**
     * default constructor, tries to instantiate a Python Interpreter
     */
    public Jython() {
        m_Interpreter = newInterpreter();
    }

    /**
     * returns the currently used Python Interpreter
     * 
     * @return the interpreter, can be null
     */
    public Object getInterpreter() {
        return m_Interpreter;
    }

    /**
     * executes the specified method on the current interpreter and returns the
     * result, if any.
     * 
     * @param methodName   the name of the method
     * @param paramClasses the classes of the parameters
     * @param paramValues  the values of the parameters
     * @return the return value of the method, if any (in that case null)
     */
    public Object invoke(String methodName, Class<?>[] paramClasses, Object[] paramValues) {
        Object result;

        result = null;
        if (getInterpreter() != null) {
            result = invoke(getInterpreter(), methodName, paramClasses, paramValues);
        }

        return result;
    }

    /**
     * returns whether the Jython classes are present or not, i.e. whether the
     * classes are in the classpath or not
     * 
     * @return whether the Jython classes are available
     */
    public static boolean isPresent() {
        return m_Present;
    }

    /**
     * initializes and returns a Python Interpreter
     * 
     * @return the interpreter or null if Jython classes not present
     */
    public static Object newInterpreter() {
        Object result;

        result = null;

        if (isPresent()) {
            try {
                result = Class.forName(CLASS_PYTHONINERPRETER).newInstance();
            } catch (Exception e) {
                e.printStackTrace();
                result = null;
            }
        }

        return result;
    }

    /**
     * loads the module and returns a new instance of it as instance of the provided
     * Java class template.
     * 
     * @param file     the Jython module file
     * @param template the template for the returned Java object
     * @return the Jython object
     */
    public static Object newInstance(File file, Class<?> template) {
        return newInstance(file, template, new File[0]);
    }

    /**
     * loads the module and returns a new instance of it as instance of the provided
     * Java class template. The paths are added to 'sys.path' - can be used if the
     * module depends on other Jython modules.
     * 
     * @param file     the Jython module file
     * @param template the template for the returned Java object
     * @param paths    additional paths to add to "sys.path"
     * @return the Jython object
     */
    public static Object newInstance(File file, Class<?> template, File[] paths) {
        Object result;
        String tempName;
        String instanceName;
        String javaClassName;
        String objectDef;
        int i;
        String[] tmpPaths;
        HashSet<String> currentPaths;
        String filename;
        Object interpreter;

        result = null;

        if (!isPresent()) {
            return result;
        }

        interpreter = newInterpreter();
        if (interpreter == null) {
            return result;
        }

        // add paths to sys.path
        if (paths.length > 0) {
            invoke(interpreter, "exec", new Class[] { String.class }, new Object[] { "import sys" });

            // determine currently set paths
            instanceName = "syspath";
            invoke(interpreter, "exec", new Class[] { String.class }, new Object[] { instanceName + " = sys.path" });
            currentPaths = new HashSet<String>();
            try {
                tmpPaths = (String[]) invoke(interpreter, "get", new Class[] { String.class, Class.class }, new Object[] { instanceName, String[].class });
                for (i = 0; i < tmpPaths.length; i++) {
                    currentPaths.add(tmpPaths[i]);
                }
            } catch (Exception ex) {
                ex.printStackTrace();
            }

            // add only new paths
            for (i = 0; i < paths.length; i++) {
                if (!currentPaths.contains(paths[i].getAbsolutePath())) {
                    invoke(interpreter, "exec", new Class[] { String.class }, new Object[] { "sys.path.append('" + paths[i].getAbsolutePath() + "')" });
                }
            }
        }

        // get object
        filename = file.getAbsolutePath();
        invoke(interpreter, "execfile", new Class[] { String.class }, new Object[] { filename });
        tempName = filename.substring(filename.lastIndexOf("/") + 1);
        tempName = tempName.substring(0, tempName.indexOf("."));
        instanceName = tempName.toLowerCase();
        javaClassName = tempName.substring(0, 1).toUpperCase() + tempName.substring(1);
        objectDef = "=" + javaClassName + "()";
        invoke(interpreter, "exec", new Class[] { String.class }, new Object[] { instanceName + objectDef });
        try {
            result = invoke(interpreter, "get", new Class[] { String.class, Class.class }, new Object[] { instanceName, template });
        } catch (Exception ex) {
            ex.printStackTrace();
        }

        return result;
    }

    /**
     * executes the specified method and returns the result, if any
     * 
     * @param o            the object the method should be called from, e.g., a
     *                     Python Interpreter
     * @param methodName   the name of the method
     * @param paramClasses the classes of the parameters
     * @param paramValues  the values of the parameters
     * @return the return value of the method, if any (in that case null)
     */
    public static Object invoke(Object o, String methodName, Class<?>[] paramClasses, Object[] paramValues) {
        Method m;
        Object result;

        result = null;

        try {
            m = o.getClass().getMethod(methodName, paramClasses);
            result = m.invoke(o, paramValues);
        } catch (Exception e) {
            e.printStackTrace();
            result = null;
        }

        return result;
    }

    /**
     * deserializes the Python Object from the stream
     * 
     * @param in the stream to use
     * @return the deserialized object
     */
    public static Object deserialize(InputStream in) {
        Class<?> cls;
        Class<?>[] paramTypes;
        Constructor<?> constr;
        Object[] arglist;
        Object obj;
        Object result;

        result = null;

        try {
            cls = Class.forName(CLASS_PYTHONOBJECTINPUTSTREAM);
            paramTypes = new Class[] { InputStream.class };
            constr = cls.getConstructor(paramTypes);
            arglist = new Object[] { in };
            obj = constr.newInstance(arglist);
            result = invoke(obj, "readObject", new Class[] {}, new Object[] {});
        } catch (Exception e) {
            e.printStackTrace();
        }

        return result;
    }

    /**
     * If no arguments are given, it just prints the presence of the Jython classes,
     * otherwise it expects a Jython filename to execute.
     * 
     * @param args commandline arguments
     */
    public static void main(String[] args) {
        if (args.length == 0) {
            System.out.println("Jython present: " + isPresent());
        } else {
            Jython jython = new Jython();
            if (jython.getInterpreter() == null) {
                System.err.println("Cannot instantiate Python Interpreter!");
            } else {
                jython.invoke("execfile", new Class[] { String.class }, new Object[] { args[0] });
            }
        }
    }
}
